package it.polito.connectedcar.vehicle.network;

import it.polito.connectedcar.general.network.ConnectedCarSocket;
import it.polito.connectedcar.general.network.EntityNetworkConnectorFactory;
import it.polito.connectedcar.general.network.NetworkConnectionManager;
import it.polito.connectedcar.message.MessageQueueFactory;
import it.polito.connectedcar.message.OutgoingMessage;
import it.polito.connectedcar.message.VehicleFromTCCMessage;
import it.polito.connectedcar.message.VehicleFromVehicleMessage;
import it.polito.connectedcar.tcc.zone.Token;
import it.polito.connectedcar.vehicle.VehicleNeighborhood;
import it.polito.connectedcar.vehicle.VehicleZone;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.util.Iterator;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * This class deals with low level network communication. It receives Message ,
 * convert it in UDP datagram and sends it to the given destination. The message
 * will contain an authenticator to be used to calculate the HMAC-SHA256 digest
 * to be appended to the datagram
 * 
 * 
 * @author Nicola Aresta
 * 
 */
public class VehicleNetworkConnectionManager
		implements
			NetworkConnectionManager {

	private ConnectedCarSocket socket;
	private Thread socketReaderThread;
	private Thread socketWriterThread;

	private VehicleNeighborhood vehicleNeighborhood;
	private BlockingQueue<OutgoingMessage> toBeSentMessagesQueue;
	private BlockingQueue<VehicleFromTCCMessage> tccSourceMessageQueue;
	private BlockingQueue<VehicleFromVehicleMessage> vehicleMessageQueue;

	public VehicleNetworkConnectionManager(
			VehicleNeighborhood vehicleNeighborhood,
			MessageQueueFactory messageQueueFactory) throws SocketException {
		this.vehicleNeighborhood = vehicleNeighborhood;
		init(messageQueueFactory);
	}

	public void init(MessageQueueFactory messageQueueFactory)
			throws SocketException {

		toBeSentMessagesQueue = messageQueueFactory.getToBeSentMessageQueue();
		tccSourceMessageQueue = messageQueueFactory.getTCCMessageQueue();
		vehicleMessageQueue = messageQueueFactory.getVehiclesMessageQueue();
		socket = EntityNetworkConnectorFactory.newFactoryInstance().getSocket();
		System.out.println("connected as : " + socket.getLocalSocketAddress());
		socketReaderThread = new Thread(new SocketReader(socket));
		socketReaderThread.start();
		socketWriterThread = new Thread(new SocketWriter(socket));
		socketWriterThread.start();
	}

	private void onReceiveDatagram(DatagramPacket packet) {
		SocketAddress sa = packet.getSocketAddress();

		/*
		 * 1) verify integrity and authentication of peer( check the DIGEST )2)
		 * verify is not a reply ( reply attack )-> verify the timestamp anc
		 * check against the last arrived datagram from the specified peer.In
		 * order to do this , we need a map with each sender address and the
		 * last timestamp ( like Kerberos )
		 * 
		 * 3)TODO : check address and port , verify is not the TCC .If address
		 * does not correspond to TCC and it is not in the neighborhood,decide
		 * what to doIf address corresponds to TCC , check content
		 * 
		 * Extract the Datagram and create a Message
		 */
		if (!(sa instanceof InetSocketAddress))
			return;

		InetSocketAddress isa = (InetSocketAddress) sa;

		try {
			ByteArrayOutputStream bas = new ByteArrayOutputStream();
			bas.write(packet.getData(), packet.getOffset(), packet.getLength());
			/*
			 * the TCCAddresses are many because the Vehicle can be in touch
			 * with 2 zones at a time
			 */
			Iterator<VehicleZone> zonesIterator = vehicleNeighborhood
					.getNeighbors().iterator();
			while (zonesIterator.hasNext()) {
				VehicleZone zone = zonesIterator.next();
				if (zone.getTccAddress().equals(isa)) {
					VehicleFromTCCMessage message = new VehicleFromTCCMessage(bas.toByteArray());
					message.setSourceAddress(zone.getTccAddress());
					tccSourceMessageQueue.offer(message);
					bas.close();
					return;
				} else if (zone.getVehicles().contains(isa)) {
					/*
					 * check if the address is known by the vehicle in one of
					 * its managed zones
					 */
					VehicleFromVehicleMessage vehicleMessage = new VehicleFromVehicleMessage(bas.toByteArray());
					vehicleMessage.setSourceAddress(isa);
					vehicleMessageQueue.offer(vehicleMessage);
					bas.close();
					return;
				}
			}
		} catch (IOException ie) {
			ie.printStackTrace();
		}
	}

	@Override
	public void sendMessage(OutgoingMessage message) throws IOException {
		toBeSentMessagesQueue.offer(message);
	}

	private class SocketReader implements Runnable {

		private ConnectedCarSocket dsocket;

		public SocketReader(ConnectedCarSocket dsocket) {
			this();
			this.dsocket = dsocket;
		}

		private SocketReader() {

		}

		@Override
		public void run() {
			int datagramLimit = 1460;
			while (!dsocket.isClosed()) {
				try {
					DatagramPacket dp = new DatagramPacket(new byte[datagramLimit], datagramLimit);
					dsocket.receive(dp);
					onReceiveDatagram(dp);
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	private class SocketWriter implements Runnable {

		private ConnectedCarSocket dsocket;

		public SocketWriter(ConnectedCarSocket dsocket) {
			this();
			this.dsocket = dsocket;
		}

		private SocketWriter() {
		}

		@Override
		public void run() {

			while (!dsocket.isClosed()) {
				try {
					OutgoingMessage m = toBeSentMessagesQueue.take();

					// FIXME the recipients shpuld be set by who created the
					// message: use getRecipientAddresses() in the Message class

					if (m.isForCurrentTCC() && !m.isForZones()) {
						// send only to the TCC;
						synchronized (vehicleNeighborhood.getCurrentZone()) {
							byte[] messageAsBytes = m.getMessageAsBytes(true,vehicleNeighborhood.getCurrentZone().getZoneToken());
							dsocket.send(new DatagramPacket(messageAsBytes,messageAsBytes.length, m.getTccAddress()));
						}
					} else {
						Iterator<VehicleZone> it = vehicleNeighborhood.getNeighbors().iterator();

						boolean sent = false;
						while (it.hasNext()) {
							VehicleZone vehicleZone = it.next();

							Token to = vehicleZone.getZoneToken();
							byte[] mAsBytes = m.getMessageAsBytes(true, to);
							/* send the message to the TCC */
							dsocket.send(new DatagramPacket(mAsBytes,mAsBytes.length, vehicleZone.getTccAddress()));

							/*
							 * send the message to all the vehicles in the zone
							 * , interesting for the current processing vehicle
							 */
							byte[] messageToVehiclesAsBytes = m.getMessageAsBytes(false, to);

							int currentPriorityForSending = vehicleZone.getCurrentPriorityForSending();
							for (int i = VehicleZone.MAXIMUM_PRIORITY; i <= currentPriorityForSending; i++) {
								CopyOnWriteArraySet<InetSocketAddress> vehicles = vehicleZone.getPrioritizedVehicles().get(i);
								if (vehicles != null) {
									Iterator<InetSocketAddress> addressesIterator = vehicles.iterator();
									while (addressesIterator.hasNext()) {
										InetSocketAddress isa = addressesIterator.next();
										if (!isa.equals(dsocket.getLocalSocketAddress())) {
											sent = true;
											dsocket.send(new DatagramPacket(
													messageToVehiclesAsBytes,
													messageToVehiclesAsBytes.length,
													isa));
										}
									}
								}
							}
							vehicleZone.updatePriorityCounter();
						}

						if (sent)
							Thread.sleep(500);

					}
					/* sleep half a second before reading again a message */
				} catch (InterruptedException e) {
					e.printStackTrace();
					break;
				} catch (SocketException e) {
					e.printStackTrace();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}

	public SocketAddress getAddress() {
		return socket.getLocalSocketAddress();
	}
}
